# This code replumbs a few classes so that ActiveRecord accepts a wider range
# of user friendly date formats. Change Date.parse_date if you need different
# date input formats. To use: drop this file into ./lib, require it into
# your model and then validate dates with the validates_dates validator.
#
# Written by Stuart Rackham <srackham@methods.co.nz>
# Current version tested on Rails 1.0.0
#

# Add date string parser class method to Date class.
#
class Date

  # Parse date string with one of the following formats:
  #
  # * ISO date format: yyyy-mm-dd, for example '2001-12-25'
  # * d[ mmm[ yy[yy]]]: examples: '22', '22 feb', '22 feb 2003',
  #   '22 feb 03', '22 February 2003'
  # * +n[units] or -n[units]: Date from today: examples: '+22', '+22 days',
  #   '+22d', '-4 weeks', '-4w', '-4week', '+6 months', '+6m', '+6month',
  #   '-2 years', '-2y'
  #
  # The string argument is first converted to a string with #to_s.
  # Returns nil if passed nil or an empty string.
  # Raises ArgumentError if string can't be parsed.
  #
  def self.parse_date(string)
    return string if string.is_a?(Date) or string.is_a?(Time)
    string = string.to_s.strip.downcase
    return nil if string.empty?
    today = Date.today
    if string =~ /^(\d{2})\/(\d{2})\/(\d{4})/ || string =~ /^(\d{4})-(\d{2})-(\d{2})/
      # MM/DD/yyyy (HH:MM:SS) || yyyy-MM-DD (HH:MM:SS).
      date_array = ParseDate.parsedate(string, true)
      [3,4,5].each do |place|
        date_array[place] = 0 if date_array[place].nil?
      end
      begin
        result = DateTime.new(date_array[0], date_array[1], date_array[2], date_array[3], date_array[4], date_array[5])
      rescue
        raise ArgumentError
      end
    else
      raise ArgumentError
    end
    result
  end

end

ActiveRecord::Validations::ClassMethods.class_eval do

  # Validates date values, these can be dates or any formats accepted by
  # Date.parse_date.
  # 
  # For example:
  #
  #   class Person < ActiveRecord::Base
  #     require_dependency 'date_validator'
  #     validates_dates :birthday,
  #                     :from => '1 Jan 1920',
  #                     :to => Date.today,
  #                     :allow_nil => true
  #   end
  #
  # Options:
  # * from - Minium allowed date. May be a date or a string recognized
  #   by Date.parse_date.
  # * to - Maxumum allowed date. May be a date or a string recognized
  #   by Date.parse_date.
  # * allow_nil - Attribute may be nil; skip validation.
  #
  def validates_dates(*attr_names)
    configuration =
      { :message => 'is an invalid date',
        :on => :save,
      }
    configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
    # Don't let validates_each handle allow_nils, it checks the cast value.
    allow_nil = configuration.delete(:allow_nil)
    from = Date.parse_date(configuration.delete(:from))
    to = Date.parse_date(configuration.delete(:to))
    validates_each(attr_names, configuration) do |record, attr_name, value|
      before_cast = record.send("#{attr_name}_before_type_cast")
      next if allow_nil and (before_cast.nil? or before_cast == '')
      begin
        date = Date.parse_date(before_cast)
      rescue
        record.errors.add(attr_name, configuration[:message])
      else
        if from and date < from
          record.errors.add(attr_name,
                            "cannot be less than #{from.strftime('%e-%b-%Y')}")
        end
        if to and date > to
          record.errors.add(attr_name,
                            "cannot be greater than #{to.strftime('%e-%b-%Y')}")
        end
      end
    end
  end

end

# Override default date type cast class method to handle Date.parse_date
# formats(the default implementation returns nil if passed an unrecognized date
# format).
#
class ActiveRecord::ConnectionAdapters::Column
  def self.string_to_date(string)
    return string unless string.is_a?(String)
    Date.parse_date(string) rescue nil
  end
end
